/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package sasp_messages;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.json.JSONException;
import org.json.JSONObject;
import sasp_protocol.Entity;
import sasp_protocol.Protocol;
import sasp_udp.UDPListener;
import sasp_udp.UDPMessage;
import sasp_udp.UDPTransceiver;

/**
 *
 * @author mauriello
 */
public class MessageHandler implements UDPListener, Runnable {

    public final Object lock = new Object();

    /* Check for Resend -- Fixed at 3 Retries Seperated by 10 Seconds */
    /* Received Replies will remove outstanding messages from the message list */
    public void run() {
        while( true ) {
            try {
                /* Update Message Resend List every Second */
                Thread.sleep(1000);
                /* Update each Message in List */
                for( int i = 0; i < messageList.size(); i++) {
                    MessageCapsule next = messageList.get(i);
                    /* If delay is up.... */
                    if( next.checkDelay() ) {
                        /* Resend Message */
                        if( next.decrementRetries() ) {
                            this.forwardCommand(next.getIp(), next.getPort(), next.getCommand());
                        /* Numer of retries is up...remove message and notify listener */
                        } else {
                            this.handleSuspectDrop(next.getDestinationId());
                            messageList.remove(next);
                        }
                    }
                }
            } catch (InterruptedException ex) {
                Logger.getLogger(MessageHandler.class.getName()).log(Level.SEVERE, null, ex);
            }

        }
    }

    /* Enum that enables the ability to switch based on SASPv1Command */
    public enum Commands
    {
        PING, REQ_TBL, REQ_SN, REQ_DATA, UPD_DATA, UPD_KEY, FIND_SN, 
        PRO_NODE, NEW_NODE, JOIN_REQUEST, SESSION_KEY, QUIT, RESET, NOVALUE;

        public static Commands toCommand(String str)
        {
            try {
                return valueOf(str);
            } 
            catch (Exception ex) {
                return NOVALUE;
            }
        }   
    }
    
    private ArrayList<MessageCapsule> messageList;

    /* Encapsulate Message for Potential Retries */
    private class MessageCapsule {
        SASPCommand command;
        Commands type;
        int retries;
        int delay;
        String ip;
        int port;

        public MessageCapsule( Commands _type, SASPCommand _command, String _ip, int _port ) {
            retries = 3; /* Retry 3 Times */
            delay = 10;  /* Wait 10 Seconds for Reply */
            command = _command;
            type = _type;
            ip = _ip;
            port = _port;
        }

        public String getIp( ) {
            return ip;
        }

        public int getPort( ) {
            return port;
        }

        /* Decrement retries count return true if retries are remaining */
        public boolean decrementRetries( ) {
            boolean retVal = true;
            
            if( retries > 0 ) { retries--; }
            else { retVal = false; }
            
            return retVal;
        }

        public int getDestinationId( ) {
            return command.getDestinationID();
        }

        public int getMessageId( ) {
            return command.getMessageID();
        }

        public SASPCommand getCommand() {
            return command;
        }

        public void setCommand(SASPCommand command) {
            this.command = command;
        }

        public Commands getType() {
            return type;
        }

        public void setType(Commands type) {
            this.type = type;
        }

        /* Decrement delay...when delay hits zero resend or drop message */
        public boolean checkDelay( ) {
            boolean retVal = false;

            if( delay > 0 ) { delay--; }
            else { retVal = true; }

            return retVal;
        }
    }

    /* Enum that enables the ability to switch based on SASPv1Reply */
    public enum Replies
    {
        ACK, REQ_SN, REQ_TBL, REQ_DATA, NACK, NOVALUE;

        public static Replies toReply(String str)
        {
            try {
                return valueOf(str);
            }
            catch (Exception ex) {
                return NOVALUE;
            }
        }
    }

    private ArrayList<MessageListener> msgListeners;
    private UDPTransceiver udpTransceiver;
    private Thread messThread;

    public MessageHandler( UDPTransceiver _t ) {
        messThread = new Thread(this);
        udpTransceiver = _t;
        msgListeners = new ArrayList<MessageListener>( );
        messageList = new ArrayList<MessageCapsule>( );
        /* Start Message List Management Thread */
        if( !messThread.isAlive() ) { messThread.start(); }
    }

    public String getSelfAddress( ) {
        String retVal = udpTransceiver.getIpAddress() + ":" + Integer.toString(udpTransceiver.getPortNumber());
        return retVal;
    }

    /* Send SuperNode Request */
    public void sendSNRequest( int _id, int _m, String _ip, int _p, String _i, String _a ) {
        REQMasterCommand command = new REQMasterCommand(_id, 1, _m, _i, _a );
        udpTransceiver.transmitUdpMessage(_ip, _p, command.toString());
        MessageCapsule messCap = new MessageCapsule( Commands.REQ_SN, command, _ip, _p );
        synchronized(lock) {
            this.messageList.add(messCap);
        }
    }

    /* Send Command to Another Node */
    public void forwardCommand( String _ip, int _p, SASPCommand _cmd ) {
        udpTransceiver.transmitUdpMessage(_ip, _p, _cmd.toString());
    }

    public void sendReqTableCommand( int _oid, int _did, int _mid, String _ip, int _p ) {
        REQTableCommand cmd = new REQTableCommand(_oid, _did, _mid);
        udpTransceiver.transmitUdpMessage(_ip, _p, cmd.toString());

        /* Add Message to Message Waiting for Replies List */
        MessageCapsule messCap = new MessageCapsule( Commands.REQ_TBL, cmd, _ip, _p );
        synchronized(lock) {
            this.messageList.add(messCap);
        }
    }

    public void sendAckReply( int _oid, int _did, int _mid, String _ip, int _p ) {
        ACKReply reply = new ACKReply(_oid, _did, _mid);
        udpTransceiver.transmitUdpMessage(_ip, _p, reply.toString());
    }

    public void sendNackReply( int _oid, int _did, int _mid, String _ip, int _p, String _r ) {
        NACKReply reply = new NACKReply(_oid, _did, _mid, _r);
        udpTransceiver.transmitUdpMessage(_ip, _p, reply.toString());
    }

    public void sendQuitCommand( int _oid, int _did, int _mid, String _ip, int _p ) {
        QuitCommand cmd = new QuitCommand( _oid, _did, _mid );
        udpTransceiver.transmitUdpMessage(_ip, _p, cmd.toString());
    }

    public void sendProNodeCommand( int _oid, int _did, int _mid, String _ip, int _p, HashSet<Entity> _d ) {
        PRONodeCommand cmd = new PRONodeCommand(_oid, _did, _mid, _d);
        udpTransceiver.transmitUdpMessage(_ip, _p, cmd.toString());
        
        /* Add Message to Message Waiting for Replies List */
        MessageCapsule messCap = new MessageCapsule( Commands.PRO_NODE, cmd, _ip, _p );
        synchronized(lock) {
            this.messageList.add(messCap);
        }

    }

    public void sendReqDataCommand( int _oid, int _did, int _mid, String _ip, int _p, String _oip, int [] _req, int [] _ign ) {
        REQDataCommand cmd = new REQDataCommand(_oid, _did, _mid, _oip, _req, _ign);
        udpTransceiver.transmitUdpMessage(_ip, _p, cmd.toString());

        /* Add Message to Message Waiting for Replies List */
        MessageCapsule messCap = new MessageCapsule( Commands.REQ_DATA, cmd, _ip, _p );
        synchronized(lock) {
            this.messageList.add(messCap);
        }
    }

    public void sendNewNodeCommand( int _oid, int _did, int _mid, String _ip, int _p, String _myIP, String _myAlias) {
        NEWSupernodeCommand cmd = new NEWSupernodeCommand(_oid, _did, _mid, _myIP, _myAlias);
        udpTransceiver.transmitUdpMessage(_ip, _p, cmd.toString());
        
        /* Add Message to Message Waiting for Replies List */
        MessageCapsule messCap = new MessageCapsule( Commands.NEW_NODE, cmd, _ip, _p );
        synchronized(lock) {
            this.messageList.add(messCap);
        }
    }

    public void sendUpdDataCommand( int _oid, int _did, int _mid, String _ip, int _p, HashSet<Entity> _d ) {
        UPDDataCommand cmd = new UPDDataCommand(_oid, _did, _mid, _d);
        udpTransceiver.transmitUdpMessage(_ip, _p, cmd.toString());
        
        /* Add Message to Message Waiting for Replies List */
        MessageCapsule messCap = new MessageCapsule( Commands.UPD_DATA, cmd, _ip, _p );
        synchronized(lock) {
            this.messageList.add(messCap);
        }
    }

    public void sendReqDataReply( int _oid, int _did, int _rmid, String _ip, int _p, HashSet<Entity> _d) {
        REQDataReply reply = new REQDataReply(_oid, _did, _rmid, _d);
        udpTransceiver.transmitUdpMessage(_ip, _p, reply.toString());
    }

    public void sendReqTableReply( int _oid, int _did, int _rmid, String _ip, int _p, HashSet<Entity> _d ) {
        REQTableReply reply = new REQTableReply(_oid, _did, _rmid, _d);
        udpTransceiver.transmitUdpMessage(_ip, _p, reply.toString());
    }

    public void sendSNReply( int _oid, int _did, int _rmid, String _ip, int _p, REQMasterTuple _body ) {
        REQMasterReply reply = new REQMasterReply(_oid, _did, _rmid, _body);
        udpTransceiver.transmitUdpMessage(_ip, _p, reply.toString());
    }

    public void sendPing( int _d ) {
        PINGCommand command = new PINGCommand(1,2,3); /* TODO: Implement Node Table and Message Count */
        udpTransceiver.transmitUdpMessage("127.0.0.1", 12345, command.toString());
    }

    public boolean addMessageListener( MessageListener _l ) {
        return msgListeners.add(_l);
    }

    public boolean removeMessageListener( MessageListener _l ) {
        return msgListeners.remove(_l);
    }

    /* Notify listeners of new command message */
    private synchronized void handleRcvCmdMessage( Commands cmd, SASPCommand _inCmd ) {
        Iterator<MessageListener> msgIter = msgListeners.iterator();
        while ( msgIter.hasNext() ){
            msgIter.next().CommandMessageHandler(cmd, _inCmd);
        }
    }

    private synchronized void handleSuspectDrop( int _ent ) {
        Iterator<MessageListener> msgIter = msgListeners.iterator();
        while ( msgIter.hasNext() ){
            //msgIter.next().SuspectDroppedEntity(_ent);
        }
    }

    /* Notify Listeners of new reply message and remove associated command from the
     * commands waiting for a reply list.
     */
    private synchronized void handleRcvRplMessage( Replies rpl, SASPReply _inRpl ) {
        /* Check Message List */
        for( int i = 0; i < messageList.size(); i++) {
            MessageCapsule next = messageList.get(i);
            /* Check for Associated Message ID in commands waiting for a reply list
             * Remove if the Message ID/DestinationID combo matches
             */
            if( _inRpl.getOriginatorID() == next.getDestinationId() && _inRpl.getReplyToID() == next.getMessageId() ) {
                    messageList.remove(next);
            }
        }

        /* Notify Listeners of Reply */
        Iterator<MessageListener> msgIter = msgListeners.iterator();
        while ( msgIter.hasNext() ){
            msgIter.next().ReplyMessageHandler(rpl, _inRpl);
        }
    }

    public void handleMessage(UDPMessage _rcvMessage) {
        try {
            JSONObject inMessage = new JSONObject(_rcvMessage.getTheMessage());

            /* Check if message is a command */
            if( inMessage.has("SASPv1Command")) {
                /* Package Command in Concrete Class and Notify Listeners */
                switch(Commands.toCommand(inMessage.getString("SASPv1Command"))) {
                    case NEW_NODE:
                        NEWSupernodeCommand nncmd = new NEWSupernodeCommand(_rcvMessage.getTheMessage());
                        this.handleRcvCmdMessage(Commands.NEW_NODE, nncmd);
                        break;
                    case PRO_NODE:
                        PRONodeCommand pncmd = new PRONodeCommand(_rcvMessage.getTheMessage());
                        this.handleRcvCmdMessage(Commands.PRO_NODE, pncmd);
                        break;
                    case QUIT:
                        QuitCommand qtcmd = new QuitCommand(_rcvMessage.getTheMessage());
                        this.handleRcvCmdMessage(Commands.QUIT, qtcmd);
                        break;
                    case UPD_DATA:
                        UPDDataCommand udcmd = new UPDDataCommand(_rcvMessage.getTheMessage());
                        this.handleRcvCmdMessage(Commands.UPD_DATA, udcmd);
                        break;
                    case PING:
                        PINGCommand cmd = new PINGCommand(_rcvMessage.getTheMessage());
                        udpTransceiver.transmitUdpMessage(_rcvMessage.getIpAddress(), 12344, cmd.execute().toString());
                        break;
                    case REQ_SN:
                        REQMasterCommand rmcmd = new REQMasterCommand(_rcvMessage.getTheMessage());
                        this.handleRcvCmdMessage(Commands.REQ_SN, rmcmd );
                        break;
                    case REQ_TBL:
                        REQTableCommand rtcmd = new REQTableCommand(_rcvMessage.getTheMessage());
                        this.handleRcvCmdMessage(Commands.REQ_TBL, rtcmd);
                        break;
                    case REQ_DATA:
                        REQDataCommand rdcmd = new REQDataCommand(_rcvMessage.getTheMessage());
                        this.handleRcvCmdMessage(Commands.REQ_DATA, rdcmd);
                        break;
                    default:
                        saspmanager.SASPManagerApp.addLine("MES - Unsupported SASP Command = " + inMessage.getString("SASPv1Command"));
                        break;
                }
                /* Check if Message is a Reply */
            } else if( inMessage.has("SASPv1Reply")) {
                /* Package Reply in Concrete Class and Notify Listeners */
                switch(Replies.toReply(inMessage.getString("SASPv1Reply"))) {
                    case NACK:
                        NACKReply nrep = new NACKReply(_rcvMessage.getTheMessage());
                        saspmanager.SASPManagerApp.addLine("PRO - Received Nack with Reason = " + nrep.getReason());
                        this.handleRcvRplMessage(Replies.NACK, nrep);
                    case ACK:
                        ACKReply rep = new ACKReply(_rcvMessage.getTheMessage());
                        this.handleRcvRplMessage(Replies.ACK, rep);
                        break;
                    case REQ_SN:
                        REQMasterReply rmrep = new REQMasterReply( _rcvMessage.getTheMessage() );
                        this.handleRcvRplMessage(Replies.REQ_SN, rmrep);
                        break;
                    case REQ_TBL:
                        REQTableReply rtrep = new REQTableReply( _rcvMessage.getTheMessage() );
                        this.handleRcvRplMessage(Replies.REQ_TBL, rtrep);
                        break;
                    case REQ_DATA:
                        REQDataReply rdrep = new REQDataReply( _rcvMessage.getTheMessage() );
                        this.handleRcvRplMessage(Replies.REQ_DATA, rdrep);
                        break;
                    default:
                        saspmanager.SASPManagerApp.addLine("MES - Unsupported SASP Reply - " + inMessage.getString("SASPv1Reply"));
                        break;
                }
            }
        } catch (JSONException ex) {
            Logger.getLogger(MessageHandler.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
}
